#pragma once

#include <unordered_map>

#include "map/poolmgr.h"

class Group;
class Map;

typedef std::set<unsigned int> CellGuidSet;

struct MapCellObjectGuids
{
    CellGuidSet creatures;
    CellGuidSet gameobjects;
};

typedef std::unordered_map < unsigned int/*cell_id*/, MapCellObjectGuids > MapCellObjectGuidsMap;

class MapPersistentStateManager;

class MapPersistentState
{
    friend class MapPersistentStateManager;
protected:
    MapPersistentState(unsigned short MapId, unsigned int InstanceId);

public:

    /* Unloaded when m_playerList and m_groupList become empty
       or when the instance is reset */
    virtual ~MapPersistentState();

    /* A map corresponding to the InstanceId/MapId does not always exist.
    MapPersistentState objects may be created on player logon but the maps are
    created and loaded only when a player actually enters the instance. */
    unsigned int GetInstanceId() const { return m_instanceid; }
    unsigned int GetMapId() const { return m_mapid; }

    dbc::MapEntry const* GetMapEntry() const;

    bool IsUsedByMap() const { return m_usedByMap; }
    Map* GetMap() const { return m_usedByMap; }         // Can be NULL if map not loaded for persistent state
    void SetUsedByMapState(Map* map)
    {
        m_usedByMap = map;
        if (!map)
        {
            UnloadIfEmpty();
        }
    }

    time_t GetCreatureRespawnTime(unsigned int loguid) const
    {
        RespawnTimes::const_iterator itr = m_creatureRespawnTimes.find(loguid);
        return itr != m_creatureRespawnTimes.end() ? itr->second : 0;
    }
    void SaveCreatureRespawnTime(unsigned int loguid, time_t t);
    time_t GetGORespawnTime(unsigned int loguid) const
    {
        RespawnTimes::const_iterator itr = m_goRespawnTimes.find(loguid);
        return itr != m_goRespawnTimes.end() ? itr->second : 0;
    }
    void SaveGORespawnTime(unsigned int loguid, time_t t);

    // pool system
    void InitPools();
    virtual SpawnedPoolData& GetSpawnedPoolData() = 0;

    template<typename T>
    bool IsSpawnedPoolObject(unsigned int db_guid_or_pool_id) { return GetSpawnedPoolData().IsSpawnedObject<T>(db_guid_or_pool_id); }

    // grid objects (Dynamic map/instance specific added/removed grid spawns from pool system/etc)
    MapCellObjectGuids const& GetCellObjectGuids(unsigned int cell_id) { return m_gridObjectGuids[cell_id]; }
    void AddCreatureToGrid(unsigned int guid, mt::CreatureData const* data);
    void RemoveCreatureFromGrid(unsigned int guid, mt::CreatureData const* data);
    void AddGameobjectToGrid(unsigned int guid, mt::GameObjectData const* data);
    void RemoveGameobjectFromGrid(unsigned int guid, mt::GameObjectData const* data);
protected:
    virtual bool CanBeUnload() const = 0;               // body provided for subclasses

    bool UnloadIfEmpty();
    void ClearRespawnTimes();
    bool HasRespawnTimes() const { return !m_creatureRespawnTimes.empty() || !m_goRespawnTimes.empty(); }

private:
    void SetCreatureRespawnTime(unsigned int loguid, time_t t);
    void SetGORespawnTime(unsigned int loguid, time_t t);

private:
    typedef std::unordered_map<unsigned int, time_t> RespawnTimes;

    unsigned int m_instanceid;
    unsigned int m_mapid;
    Map* m_usedByMap;                                   // NULL if map not loaded, non-NULL lock MapPersistentState from unload

    // persistent data
    RespawnTimes m_creatureRespawnTimes;                // lock MapPersistentState from unload, for example for temporary bound dungeon unload delay
    RespawnTimes m_goRespawnTimes;                      // lock MapPersistentState from unload, for example for temporary bound dungeon unload delay
    MapCellObjectGuidsMap m_gridObjectGuids;            // Single map copy specific grid spawn data, like pool spawns
};

inline bool MapPersistentState::CanBeUnload() const
{
    // prevent unload if used for loaded map
    return !m_usedByMap;
}

class WorldPersistentState : public MapPersistentState
{
public:
    /* Created either when:
       - any new non-instanceable map created
       - respawn data loading for non-instanceable map
    */
    explicit WorldPersistentState(unsigned short MapId) : MapPersistentState(MapId, 0) {}

    ~WorldPersistentState() {}

    SpawnedPoolData& GetSpawnedPoolData() override { return m_sharedSpawnedPoolData; }
protected:
    bool CanBeUnload() const override;                  // overwrite MapPersistentState::CanBeUnload

private:
    static SpawnedPoolData m_sharedSpawnedPoolData;     // Pools spawns state for map, shared by all non-instanced maps
};

/*
    Holds the information necessary for creating a new map for an existing instance
    Is referenced in three cases:
    - player-instance binds for solo players (not in group)
    - player-instance binds for permanent raid saves
    - group-instance binds (both solo and permanent) cache the player binds for the group leader

    Used for InstanceMap only
*/
class DungeonPersistentState : public MapPersistentState
{
public:
    /* Created either when:
       - any new instance is being generated
       - the first time a player bound to InstanceId logs in
       - when a group bound to the instance is loaded */
    DungeonPersistentState(unsigned short MapId, unsigned int InstanceId, time_t resetTime, bool canReset);

    ~DungeonPersistentState();

    SpawnedPoolData& GetSpawnedPoolData() override { return m_spawnedPoolData; }

    mt::_Instance const* GetTemplate() const;

    unsigned char GetPlayerCount() const { return (unsigned char)m_playerList.size(); }
    unsigned char GetGroupCount() const { return (unsigned char)m_groupList.size(); }

    /* online players bound to the instance (perm/solo)
       does not include the members of the group unless they have permanent saves */
    void AddPlayer(battle::object::Player* player) { m_playerList.push_back(player); }
    bool RemovePlayer(battle::object::Player* player) { m_playerList.remove(player); return UnloadIfEmpty(); }
    /* all groups bound to the instance */
    void AddGroup(Group* group) { m_groupList.push_back(group); }
    bool RemoveGroup(Group* group) { m_groupList.remove(group); return UnloadIfEmpty(); }

    /* for normal instances this corresponds to max(creature respawn time) + X hours
       for raid instances this caches the global respawn time for the map */
    time_t GetResetTime() const { return m_resetTime; }
    void SetResetTime(time_t resetTime) { m_resetTime = resetTime; }
    time_t GetResetTimeForDB() const;

    /* instances can not be reset (except at the global reset time)
       if there are players permanently bound to it
       this is cached for the case when those players are offline */
    bool CanReset() const { return m_canReset; }
    void SetCanReset(bool canReset) { m_canReset = canReset; }

    /* Saved when the instance is generated for the first time */
    void SaveToDB();
    /* When the instance is being reset (permanently deleted) */
    void DeleteFromDB();
    /* Delete respawn data at dungeon reset */
    void DeleteRespawnTimes();
    /* Remove players bind to this state */
    void UnbindThisState();

protected:
    bool CanBeUnload() const override;                  // overwrite MapPersistentState::CanBeUnload
    bool HasBounds() const { return !m_playerList.empty() || !m_groupList.empty(); }

private:
    typedef std::list<battle::object::Player*> PlayerListType;
    typedef std::list<Group*> GroupListType;

    time_t m_resetTime;
    bool m_canReset;

    /* the only reason the instSave-object links are kept is because
       the object-instSave links need to be broken at reset time
       TODO: maybe it's enough to just store the number of players/groups */
    PlayerListType m_playerList;                        // lock MapPersistentState from unload
    GroupListType m_groupList;                          // lock MapPersistentState from unload

    SpawnedPoolData m_spawnedPoolData;                  // Pools spawns state for map copy
};

class BattleGroundPersistentState : public MapPersistentState
{
public:
    /* Created either when:
       - any new BG/arena is being generated
    */
    BattleGroundPersistentState(unsigned short MapId, unsigned int InstanceId)
        : MapPersistentState(MapId, InstanceId) {
    }

    ~BattleGroundPersistentState() {}

    SpawnedPoolData& GetSpawnedPoolData() override { return m_spawnedPoolData; }
protected:
    bool CanBeUnload() const override;                  // overwrite MapPersistentState::CanBeUnload

private:
    SpawnedPoolData m_spawnedPoolData;                  // Pools spawns state for map copy
};

enum ResetEventType
{
    RESET_EVENT_NORMAL_DUNGEON = 0,                    // no fixed reset time
    RESET_EVENT_INFORM_1 = 1,                    // raid/heroic warnings
    RESET_EVENT_INFORM_2 = 2,
    RESET_EVENT_INFORM_3 = 3,
    RESET_EVENT_INFORM_LAST = 4,
    RESET_EVENT_FORCED_INFORM_1 = 5,
    RESET_EVENT_FORCED_INFORM_2 = 6,
    RESET_EVENT_FORCED_INFORM_3 = 7,
    RESET_EVENT_FORCED_INFORM_LAST = 8,
};

enum InstanceResetFailReason
{
    INSTANCERESET_FAIL_GENERAL = 0,
    INSTANCERESET_FAIL_OFFLINE = 1,
    INSTANCERESET_FAIL_ZONING = 2,
    INSTANCERESET_FAIL_SILENTLY = 3 // as well as any above this
};

#define MAX_RESET_EVENT_TYPE   9

/* resetTime is a global propery of each (raid/heroic) map
    all instances of that map reset at the same time */
struct DungeonResetEvent
{
    ResetEventType type : 8;                              // if RESET_EVENT_DUNGEON then InstanceID == 0 and applied to all instances for map)
    unsigned short mapid;                                           // used with mapid used as for select reset for global cooldown instances (instanceid==0 for event)
    unsigned int instanceId;                                      // used for select reset for normal dungeons

    DungeonResetEvent() : type(RESET_EVENT_NORMAL_DUNGEON), mapid(0), instanceId(0) {}
    DungeonResetEvent(ResetEventType t, unsigned int _mapid, unsigned int _instanceid)
        : type(t), mapid(_mapid), instanceId(_instanceid) {
    }

    bool operator == (const DungeonResetEvent& e) { return e.mapid == mapid && e.instanceId == instanceId; }
};

class DungeonResetScheduler
{
public:                                                 // constructors
    explicit DungeonResetScheduler(MapPersistentStateManager& mgr) : m_InstanceSaves(mgr) {}
    void LoadResetTimes();

public:                                                 // accessors
    time_t GetResetTimeFor(unsigned int mapid) { return m_resetTimeByMapId[mapid]; }

    static unsigned int GetMaxResetTimeFor(mt::_Instance const* temp);
    static time_t CalculateNextResetTime(mt::_Instance const* temp, time_t prevResetTime);
public:                                                 // modifiers
    void SetResetTimeFor(unsigned int mapid, time_t t)
    {
        m_resetTimeByMapId[mapid] = t;
    }

    void ScheduleReset(bool add, time_t time, DungeonResetEvent event);

    void Update();

    void ResetAllRaid();
private:                                                // fields
    MapPersistentStateManager& m_InstanceSaves;

    // fast lookup for reset times (always use existing functions for access/set)
    typedef std::vector < time_t /*resetTime*/ > ResetTimeVector;
    ResetTimeVector m_resetTimeByMapId;

    typedef std::multimap < time_t /*resetTime*/, DungeonResetEvent > ResetTimeQueue;
    ResetTimeQueue m_resetTimeQueue;
};

class MapPersistentStateManager : public cftf::util::Singleton<MapPersistentStateManager>
{
    friend class DungeonResetScheduler;
public:                                                 // constructors
    MapPersistentStateManager();
    ~MapPersistentStateManager();

public:                                                 // common for all MapPersistentState (sub)classes
    // For proper work pool systems with shared pools state for non-instanceable maps need
    // load persistent map states for any non-instanceable maps before init pool system
    void InitWorldMaps();
    void LoadCreatureRespawnTimes();
    void LoadGameobjectRespawnTimes();

    // auto select appropriate MapPersistentState (sub)class by MapEntry, and autoselect appropriate way store (by instance/map id)
    // always return != NULL
    MapPersistentState* AddPersistentState(const dbc::MapEntry* mapEntry, unsigned int instanceId, time_t resetTime, bool canReset, bool load = false, bool initPools = true);

    // search stored state, can be NULL in result
    MapPersistentState* GetPersistentState(unsigned int mapId, unsigned int InstanceId);

    void RemovePersistentState(unsigned int mapId, unsigned int instanceId);

    template<typename Do>
    void DoForAllStatesWithMapId(unsigned int mapId, Do& _do);

public:                                                 // DungeonPersistentState specific
    void CleanupInstances();
    void PackInstances();

    DungeonResetScheduler& GetScheduler() { return m_Scheduler; }

    static void DeleteInstanceFromDB(unsigned int instanceid);

    void GetStatistics(unsigned int& numStates, unsigned int& numBoundPlayers, unsigned int& numBoundGroups);

    void Update() { m_Scheduler.Update(); }
private:
    typedef std::unordered_map < unsigned int /*InstanceId or MapId*/, MapPersistentState* > PersistentStateMap;

    //  called by scheduler for DungeonPersistentStates
    void _ResetOrWarnAll(unsigned int mapid, bool warn, unsigned int timeleft);
    void _ResetInstance(unsigned int mapid, unsigned int instanceId);
    void _CleanupExpiredInstancesAtTime(time_t t);

    void _ResetSave(PersistentStateMap& holder, PersistentStateMap::iterator& itr);
#if 0
    void _DelHelper(DatabaseType& db, const char* fields, const char* table, const char* queryTail, ...);
#endif

    // used during global instance resets
    bool lock_instLists;
    // fast lookup by instance id for instanceable maps
    PersistentStateMap m_instanceSaveByInstanceId;
    // fast lookup by map id for non-instanceable maps
    PersistentStateMap m_instanceSaveByMapId;

    DungeonResetScheduler m_Scheduler;
};

template<typename Do>
inline void MapPersistentStateManager::DoForAllStatesWithMapId(unsigned int mapId, Do& _do)
{
    auto mapEntry = dbc::MapEntry::GetElement(mapId);
    if (!mapEntry)
    {
        return;
    }

    if (mapEntry->Instanceable())
    {
        for (PersistentStateMap::iterator itr = m_instanceSaveByInstanceId.begin(); itr != m_instanceSaveByInstanceId.end();)
        {
            if (itr->second->GetMapId() == mapId)
            {
                _do((itr++)->second);
            }
            else
            {
                ++itr;
            }
        }
    }
    else
    {
        if (MapPersistentState* state = GetPersistentState(mapId, 0))
        {
            _do(state);
        }
    }
}

